/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.cf.cst;

import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Class;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Double;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Fieldref;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Float;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Integer;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_InterfaceMethodref;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Long;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Methodref;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_NameAndType;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_String;
import static com.android.dx.cf.cst.ConstantTags.CONSTANT_Utf8;

import java.util.BitSet;

import com.android.dx.cf.iface.ParseException;
import com.android.dx.cf.iface.ParseObserver;
import com.android.dx.rop.cst.Constant;
import com.android.dx.rop.cst.CstDouble;
import com.android.dx.rop.cst.CstFieldRef;
import com.android.dx.rop.cst.CstFloat;
import com.android.dx.rop.cst.CstInteger;
import com.android.dx.rop.cst.CstInterfaceMethodRef;
import com.android.dx.rop.cst.CstLong;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.rop.cst.CstNat;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.cst.StdConstantPool;
import com.android.dx.rop.type.Type;
import com.android.dx.util.ByteArray;
import com.android.dx.util.Hex;

/**
 * Parser for a constant pool embedded in a class file.
 */
public final class ConstantPoolParser {

	/** {@code non-null;} the bytes of the constant pool */
	private final ByteArray bytes;

	/** {@code non-null;} actual parsed constant pool contents */
	private final StdConstantPool pool;

	/** {@code non-null;} byte offsets to each cst */
	private final int[] offsets;

	/**
	 * -1 || &gt;= 10; the end offset of this constant pool in the
	 * {@code byte[]} which it came from or {@code -1} if not yet parsed
	 */
	private int endOffset;

	/** {@code null-ok;} parse observer, if any */
	private ParseObserver observer;

	/**
	 * Constructs an instance.
	 * 
	 * @param bytes
	 *            {@code non-null;} the bytes of the file
	 */
	public ConstantPoolParser(ByteArray bytes) {
		int size = bytes.getUnsignedShort(8); // constant_pool_count

		this.bytes = bytes;
		this.pool = new StdConstantPool(size);
		this.offsets = new int[size];
		this.endOffset = -1;
	}

	/**
	 * Sets the parse observer for this instance.
	 * 
	 * @param observer
	 *            {@code null-ok;} the observer
	 */
	public void setObserver(ParseObserver observer) {
		this.observer = observer;
	}

	/**
	 * Gets the end offset of this constant pool in the {@code byte[]} which it
	 * came from.
	 * 
	 * @return {@code >= 10;} the end offset
	 */
	public int getEndOffset() {
		parseIfNecessary();
		return endOffset;
	}

	/**
	 * Gets the actual constant pool.
	 * 
	 * @return {@code non-null;} the constant pool
	 */
	public StdConstantPool getPool() {
		parseIfNecessary();
		return pool;
	}

	/**
	 * Runs {@link #parse} if it has not yet been run successfully.
	 */
	private void parseIfNecessary() {
		if (endOffset < 0) {
			parse();
		}
	}

	/**
	 * Does the actual parsing.
	 */
	private void parse() {
		determineOffsets();

		if (observer != null) {
			observer.parsed(bytes, 8, 2,
					"constant_pool_count: " + Hex.u2(offsets.length));
			observer.parsed(bytes, 10, 0, "\nconstant_pool:");
			observer.changeIndent(1);
		}

		/*
		 * Track the constant value's original string type. True if constants[i]
		 * was a CONSTANT_Utf8, false for any other type including
		 * CONSTANT_string.
		 */
		BitSet wasUtf8 = new BitSet(offsets.length);

		for (int i = 1; i < offsets.length; i++) {
			int offset = offsets[i];
			if ((offset != 0) && (pool.getOrNull(i) == null)) {
				parse0(i, wasUtf8);
			}
		}

		if (observer != null) {
			for (int i = 1; i < offsets.length; i++) {
				Constant cst = pool.getOrNull(i);
				if (cst == null) {
					continue;
				}
				int offset = offsets[i];
				int nextOffset = endOffset;
				for (int j = i + 1; j < offsets.length; j++) {
					int off = offsets[j];
					if (off != 0) {
						nextOffset = off;
						break;
					}
				}
				String human = wasUtf8.get(i) ? Hex.u2(i) + ": utf8{\""
						+ cst.toHuman() + "\"}" : Hex.u2(i) + ": "
						+ cst.toString();
				observer.parsed(bytes, offset, nextOffset - offset, human);
			}

			observer.changeIndent(-1);
			observer.parsed(bytes, endOffset, 0, "end constant_pool");
		}
	}

	/**
	 * Populates {@link #offsets} and also completely parse utf8 constants.
	 */
	private void determineOffsets() {
		int at = 10; // offset from the start of the file to the first cst
		int lastCategory;

		for (int i = 1; i < offsets.length; i += lastCategory) {
			offsets[i] = at;
			int tag = bytes.getUnsignedByte(at);
			switch (tag) {
			case CONSTANT_Integer:
			case CONSTANT_Float:
			case CONSTANT_Fieldref:
			case CONSTANT_Methodref:
			case CONSTANT_InterfaceMethodref:
			case CONSTANT_NameAndType: {
				lastCategory = 1;
				at += 5;
				break;
			}
			case CONSTANT_Long:
			case CONSTANT_Double: {
				lastCategory = 2;
				at += 9;
				break;
			}
			case CONSTANT_Class:
			case CONSTANT_String: {
				lastCategory = 1;
				at += 3;
				break;
			}
			case CONSTANT_Utf8: {
				lastCategory = 1;
				at += bytes.getUnsignedShort(at + 1) + 3;
				break;
			}
			default: {
				ParseException ex = new ParseException("unknown tag byte: "
						+ Hex.u1(tag));
				ex.addContext("...while preparsing cst " + Hex.u2(i)
						+ " at offset " + Hex.u4(at));
				throw ex;
			}
			}
		}

		endOffset = at;
	}

	/**
	 * Parses the constant for the given index if it hasn't already been parsed,
	 * also storing it in the constant pool. This will also have the side effect
	 * of parsing any entries the indicated one depends on.
	 * 
	 * @param idx
	 *            which constant
	 * @return {@code non-null;} the parsed constant
	 */
	private Constant parse0(int idx, BitSet wasUtf8) {
		Constant cst = pool.getOrNull(idx);
		if (cst != null) {
			return cst;
		}

		int at = offsets[idx];

		try {
			int tag = bytes.getUnsignedByte(at);
			switch (tag) {
			case CONSTANT_Utf8: {
				cst = parseUtf8(at);
				wasUtf8.set(idx);
				break;
			}
			case CONSTANT_Integer: {
				int value = bytes.getInt(at + 1);
				cst = CstInteger.make(value);
				break;
			}
			case CONSTANT_Float: {
				int bits = bytes.getInt(at + 1);
				cst = CstFloat.make(bits);
				break;
			}
			case CONSTANT_Long: {
				long value = bytes.getLong(at + 1);
				cst = CstLong.make(value);
				break;
			}
			case CONSTANT_Double: {
				long bits = bytes.getLong(at + 1);
				cst = CstDouble.make(bits);
				break;
			}
			case CONSTANT_Class: {
				int nameIndex = bytes.getUnsignedShort(at + 1);
				CstString name = (CstString) parse0(nameIndex, wasUtf8);
				cst = new CstType(Type.internClassName(name.getString()));
				break;
			}
			case CONSTANT_String: {
				int stringIndex = bytes.getUnsignedShort(at + 1);
				cst = parse0(stringIndex, wasUtf8);
				break;
			}
			case CONSTANT_Fieldref: {
				int classIndex = bytes.getUnsignedShort(at + 1);
				CstType type = (CstType) parse0(classIndex, wasUtf8);
				int natIndex = bytes.getUnsignedShort(at + 3);
				CstNat nat = (CstNat) parse0(natIndex, wasUtf8);
				cst = new CstFieldRef(type, nat);
				break;
			}
			case CONSTANT_Methodref: {
				int classIndex = bytes.getUnsignedShort(at + 1);
				CstType type = (CstType) parse0(classIndex, wasUtf8);
				int natIndex = bytes.getUnsignedShort(at + 3);
				CstNat nat = (CstNat) parse0(natIndex, wasUtf8);
				cst = new CstMethodRef(type, nat);
				break;
			}
			case CONSTANT_InterfaceMethodref: {
				int classIndex = bytes.getUnsignedShort(at + 1);
				CstType type = (CstType) parse0(classIndex, wasUtf8);
				int natIndex = bytes.getUnsignedShort(at + 3);
				CstNat nat = (CstNat) parse0(natIndex, wasUtf8);
				cst = new CstInterfaceMethodRef(type, nat);
				break;
			}
			case CONSTANT_NameAndType: {
				int nameIndex = bytes.getUnsignedShort(at + 1);
				CstString name = (CstString) parse0(nameIndex, wasUtf8);
				int descriptorIndex = bytes.getUnsignedShort(at + 3);
				CstString descriptor = (CstString) parse0(descriptorIndex,
						wasUtf8);
				cst = new CstNat(name, descriptor);
				break;
			}
			}
		} catch (ParseException ex) {
			ex.addContext("...while parsing cst " + Hex.u2(idx) + " at offset "
					+ Hex.u4(at));
			throw ex;
		} catch (RuntimeException ex) {
			ParseException pe = new ParseException(ex);
			pe.addContext("...while parsing cst " + Hex.u2(idx) + " at offset "
					+ Hex.u4(at));
			throw pe;
		}

		pool.set(idx, cst);
		return cst;
	}

	/**
	 * Parses a utf8 constant.
	 * 
	 * @param at
	 *            offset to the start of the constant (where the tag byte is)
	 * @return {@code non-null;} the parsed value
	 */
	private CstString parseUtf8(int at) {
		int length = bytes.getUnsignedShort(at + 1);

		at += 3; // Skip to the data.

		ByteArray ubytes = bytes.slice(at, at + length);

		try {
			return new CstString(ubytes);
		} catch (IllegalArgumentException ex) {
			// Translate the exception
			throw new ParseException(ex);
		}
	}
}
